package lambdasinaction.chap6.my;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 分组 一个常见的数据库操作就是一个或多个属性对集合中的项目进行分组 就像前面讲到按货币对交易进行分组的例子一样 如果用指令式风格实现的话，这个操作可能会很麻烦、啰嗦而且容易出错 但是，如果用
 * Java8所推崇的函数式风格来重写的话，就很容易转化为一个非常容易看懂的语句
 *
 * @author Fish
 * @since 0.0.1 2018/7/26 23:48
 */
public class MyGrouping {

  public static void main(String[] args) {

    // {OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}
    // {其他=[薯条，米饭，时令水果，披萨]，肉=[猪肉，牛肉，鸡肉]，鱼=[虾，三文鱼]]
//    System.out.println("MyDishes grouped by type:" + groupDishesByType());

    // Dishes grouped by caloric level:{NORMAL=[beef, french fries, rice, pizza, salmon], FAT=[pork], DIET=[chicken, season fruit, prawns]}
//    System.out.println("Dishes grouped by caloric level:" + groupDishesByCaloricLevel());

    // Dishes grouped by type and caloric level:{FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[season fruit], NORMAL=[french fries, rice, pizza]}, MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}}
//    System.out.println("Dishes grouped by type and caloric level:" + groupDishedByTypeAndCaloricLevel());
    // Count dishes in groups:{MEAT=3, OTHER=4, FISH=2}
//    System.out.println("Count dishes in groups:" + countDishesInGroups());

//    System.out.println("mostCaloricByType: " + mostCaloricByType());
//    System.out.println("caloricLevelsByType:" + caloricLevelsByType());
  }

  /**
   * 分类函数不一定像方法引用那样可用，因为你想以分类的条件可能比简单的属性访问器要复杂。 例如，你可能想把热量不到400卡路里的菜划分为“低热量”（diet）,热量400到700卡路里的菜划为“普通”（normal）
   * 高于700卡路里的划为“高热量”（fat）。 由于 Dish 类的作者没有把这个操作写成一个方法，你无法使用方法引用，但你可以把这个逻辑写成 Lambda 表达式：
   */
  private static Map<CaloricLevel, List<MyDish>> groupDishesByCaloricLevel() {
    return MyDish.menu.stream().collect(
        Collectors.groupingBy(dish -> {
          if (dish.getCalories() <= 400) {
            return CaloricLevel.DIET;
          } else if (dish.getCalories() <= 700) {
            return CaloricLevel.NORMAL;
          } else {
            return CaloricLevel.FAT;
          }
        })
    );
  }

  /**
   * 假设你要把菜单中的菜按照类型进行分类，有肉的放一组，有鱼的放一组，其他的都放另一组 用 Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成这项任务
   * 这里，你给groupingBy方法传递了一个 Function(以方法引用的形式)， 它提取了流中每一道 MyDish的 MyDish.Type. 我们把这个
   * Function叫做分类函数，因为它用来把流中的元素分成不同的组 分类操作的结果是一个 map,把分组函数返回的值作为映射的键，把流中所有 具有这个分类值的项目的列表作为对应的映射值。
   * 在菜单分类的例子中，键就是菜的类型，值就是包含所有对应类型的菜肴的列表。
   */
  private static Map<MyDish.MyType, List<MyDish>> groupDishesByType() {
    return MyDish.menu.stream().collect(Collectors.groupingBy(MyDish::getType));
  }

  /**
   * 多级分组 要实现多级分组，我们可以使用一个由双参数版本的 Collectors.groupingBy 工厂方法创建的收集器， 它处理普通的分类函数之外，还可以接受 collector
   * 类型的第二个参数。 那么要进行二级分组的话，我们可以把一个内层 groupingBy传递给外层 groupingBy，并定义一个为流中项目分类的二级标准
   * 这个二级分组的结果就是像下面这样的两极 Map: {FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[season fruit],
   * NORMAL=[french fries, rice, pizza]}, MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}} 这里的外层Map
   * 的键就是第一级分类函数生成的值：“FISH, MEAT, OTHER”，而这个 Map的值又是一个 Map，键是二级分类函数生成的值：”noraml,diet,fat” 最后，第二级
   * map的值时流中元素构成的 List，是分别应用第一级和第二级分类函数所得到的对应第一级和第二级的值： “salmon、pizza”这种多级分组操作可以扩展至任意层级，n
   * 级分组就会得到一个代表 n 级树形结构的 n 级 Map. 一般来说，把 groupingBy 看作“桶”比较容易明白。第一个 groupingBy给每个键建立了一个桶。
   * 然后再用下游的收集器去收集每个桶中的元素，以此得到 n级分组。
   */
  private static Map<MyDish.MyType, Map<CaloricLevel, List<MyDish>>> groupDishedByTypeAndCaloricLevel() {
    return MyDish.menu.stream().collect(
        Collectors.groupingBy(MyDish::getType, // 一级分类函数
            Collectors.groupingBy((MyDish dish) -> { // 二级分类函数
              if (dish.getCalories() <= 400) {
                return CaloricLevel.DIET;
              } else if (dish.getCalories() <= 700) {
                return CaloricLevel.NORMAL;
              } else {
                return CaloricLevel.FAT;
              }
            }))
    );
  }


  /**
   * 例如，要数一数菜单中每类菜有多少个，可以传递 counting 收集器作为 groupingBy 收集器的第二个参数
   */
  private static Map<MyDish.MyType, Long> countDishesInGroups() {
    return MyDish.menu.stream()
        .collect(Collectors.groupingBy(MyDish::getType, Collectors.counting()));
  }

  /**
   * 这个分组的结果显然是一个 map，以 MyDish 的类型作为键，以包装了该类型中热量最高的 MyDish 的 Optional<MyDish> 作为值：
   * mostCaloricByType: {OTHER=Optional[pizza], FISH=Optional[salmon], MEAT=Optional[pork]} 这个
   * Map中的值时 Optional，因为这是 maxBy工厂方法生成的收集器的类型，但实际上，如果菜单中没有某一类型的 Dish, 这个类型就不会对应一个
   * Optional，empty()值，而且根本不会出现在 Map 的键中。 groupingBy收集器只有在应用分组条件后，第一次在流中找到某个键对应的元素时才会把键加入分组 Map 中。
   * 这意味着 Optional 包装器在这里不是很有用，因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。
   */
  private static Map<MyDish.MyType, Optional<MyDish>> mostCaloricByType() {
    return MyDish.menu.stream()
        .collect(Collectors.groupingBy(MyDish::getType, Collectors.maxBy(Comparator
            .comparingInt(MyDish::getCalories))));
  }

  /**
   * 把收集器的结果转换为另一种类型 因为分组操作的 Map 结果中的每个值上包装的 Optional 没什么用，所以你可能想要把它们去掉。
   * 要做到这一点，或者更一般地来说，把收集器返回的结果转换为另一种类型， 你可以使用 Collectors.collectingAndThen 工厂方法返回的收集器
   */
  private static Map<MyDish.MyType, MyDish> mostCaloricByTypeGet() {
    return MyDish.menu.stream().collect(Collectors.groupingBy(MyDish::getType, // 分类函数
        Collectors
            .collectingAndThen(Collectors.maxBy(Comparator.comparingInt(MyDish::getCalories)),
                // 包装后的收集器
                Optional::get))); // 转换函数
  }

  /**
   * 例如，你还重用求出所有菜肴热量总和的收集器，不过这次是对每一组 Dish 求和。
   * @return
   */
  private static Map<MyDish.MyType, Integer> sumCaloriesByType() {
    return MyDish.menu.stream().collect(
        Collectors.groupingBy(MyDish::getType, Collectors.summingInt(MyDish::getCalories)));
  }


  /**
   * 常常和 groupingBy联合使用的另一个收集器是 mapping 方法生成的。
   * 这个方法接受两个参数：一个函数对流中的元素做变换，另一个则将变换的结果对象收集起来。
   * 其目的是在累加之前对每个输入元素应用一个映射函数，这样就可以让接受特定类型元素的收集器适应不同类型的对象。
   * 我们来看一个使用这个收集器的实际例子。
   * 比方说你想要知道，对于每种类型的 Dish,菜单中都有哪些 CaloricLevel。
   * 我们可以把 groupingBy和mapping 收集器结合起来
   * 这里，就像我们前面见到过的，传递给映射方法的转换函数将 Dish 映射成了它的 CaloricLevel:
   * 生成的 CaloricLevel 流传递给一个 toSet 收集器，它和 toList 类似，不过是把流中的元素累积到
   * 一个 Set 而不是 List 中，以便仅保留各不相同的值。
   * 如先前的示例所示，这个映射收集器将会收集分组函数生成的各个子流中的元素，让你得到这样的 Map 结果：
   *
   * @return
   */
  private static Map<MyDish.MyType, Set<CaloricLevel>> caloricLevelsByType() {
    return MyDish.menu.stream().collect(Collectors.groupingBy(MyDish::getType, Collectors.mapping(
        myDish -> {
          if (myDish.getCalories() <= 400) return CaloricLevel.DIET;
          else if (myDish.getCalories() <= 700) return CaloricLevel.NORMAL;
          else return CaloricLevel.FAT;
        }, Collectors.toSet()
    )));
  }


//  private static Map<MyDish.MyType, Set<CaloricLevel>> caloricLevelsByType() {
//    return MyDish.menu.stream().collect(Collectors.groupingBy(
//        MyDish::getType, Collectors.mapping(
//            dish -> {
//              if (dish.getCalories() <= 400) return CaloricLevel.DIET;
//              else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
//              else return CaloricLevel.FAT;
//            },
//            Collectors.toCollection(HashSet::new)
//        )
//    ));
//  }

  enum CaloricLevel {DIET, NORMAL, FAT}


}
